using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Reflection;
using System.Xml;
using System.Xml.Schema;
using Fuzzware.Common;
using Fuzzware.Common.Encoding;
using Fuzzware.Common.DataSchema;
using Fuzzware.Common.DataSchema.Restrictions;
using Fuzzware.Common.XML;
using Fuzzware.Common.XML.Restrictions;

namespace Fuzzware.ConvertFromXML.Processors
{
    /// <summary>
    /// The purpose of this class is to abstract out the schema attribute instructions and the XML processing instructions from the 
    /// processing of the XML, in this way it should be easier to add more instructions in the future.
    /// </summary>
    public class NodeProcessor : IDisposable
    {
        // See ProcessorFnAssigner for a brief description of these functions
        //public delegate void NodeProcessingFn(ref byte[] operand, String PItext, Coder.EncodingType NodeEncType);
        public delegate void NodeProcessingFn(ref byte[] operand, IOutputNode SourceNode, IOutputNode TargetNode);

        public const String Markup = "markup";
        public enum MarkupType
        {
            remove,
            removeIncludingChildNodes,
            dontRemove,
            dontRemoveIncludingChildNodes
        }

        //public const String OutputAs = "outputAs";
        //public enum OutputAsType
        //{
        //    Unchanged,
        //    Decoded,
        //    Base64,
        //    Hex,
        //    MSBase64,
        //    BinaryLittleEndian,
        //    BinaryBigEndian
        //}

        // Processing Commands
        MarkupType writeMarkup;                     // Do we write XML markup on output?
        Coder.EncodingType encodingType;            // How is the node currently encoded?
        public Coder.OutputAsType outputType;       // How do we encode it on output?
        public DataSchemaTypeCode TypeCode;             // What is the type (if it's a simple type)?
        public bool bDontOutput;                      // Suppress the serialisation of this node and child nodes
        
        public List<String> NodeId;                       // The ID another node will use if its value depends on this one.
        public String RefId;                        // The ID of another node this node is a function of.
        public String Order;                        // A number specifying the order of processing.
        public bool TargetNode;
        public NodeProcessingFn ProcessingFn;       // The function used to process the node with id RefId.
        public String PItext;                       // Concatenated string of all the Processing Instructions text

        OutputSettings oSettings;
        XmlSchemaSet oSchemaSet; 
        XmlQualifiedName Name;
        Encoding oEncoding;                         // The output format of the XML (e.g. Unicode, UTF-8, UTF-7 etc)
        OutputNode Owner;
        XMLElementIdentifier oEleId;

        public NodeProcessor(XmlQualifiedName NodeName, OutputSettings Settings, OutputNode owner)
        {
            oSettings = Settings;
            oSchemaSet = Settings.XMLDoc.Schemas;
            Name = NodeName;
            oEncoding = Settings.XmlSettings.Encoding;
            Owner = owner;
            oEleId = null;
            XmlSchemaElement oXmlSchemaElement = Owner.XPathNav.SchemaInfo.SchemaElement;
            if (null == oXmlSchemaElement)
                Log.Write(MethodBase.GetCurrentMethod(), "Cannot get schema information from compiled XML for '" + NodeName + "', default SchemaAttributeCommands will be used.", Log.LogType.LogOnlyInfo);
            else
                oEleId = new XMLElementIdentifier(Name, oXmlSchemaElement, oSchemaSet, oXmlSchemaElement.MinOccurs, oXmlSchemaElement.MaxOccurs);
            NodeId = new List<string>();

            // Set defaults
            writeMarkup = MarkupType.dontRemoveIncludingChildNodes;
            encodingType = Coder.EncodingType.None;
            outputType = Coder.OutputAsType.Unchanged;
            TypeCode = DataSchemaTypeCode.None;
            Order = "100";

            SetSchemaAttributeInstructions();
        }


        #region IDisposable Members

        /// <summary>
        /// Call this to free up all the resources of the object
        /// </summary>
        public void Dispose()
        {
            if (null != NodeId)
                NodeId.Clear();
            NodeId = null;
            ProcessingFn = null;
            oSettings = null;
            oSchemaSet = null;
            Owner = null;
            oEleId = null;
            GC.SuppressFinalize(this);
        }

        #endregion

        private void SetMarkup()
        {
            // If the node explicitly says too, then do it.  Or, if no parent then we get back the default.
            MarkupType NodeWriteMarkup;

            if (SchemaAttributeCommands.GetRemoveMarkupOnOutput(oSettings, oEleId, out NodeWriteMarkup))
            {
                // Node has explicitly stated what to do
                this.writeMarkup = NodeWriteMarkup;
                return;
            }

            // Node has not explicitly stated what to do.

            // Do what parent node does (or the first ancestor that instructs children)
            OutputNode ParentNode = Owner.Parent;
            while ((ParentNode != null) && ((ParentNode.Processor.WriteMarkup == MarkupType.remove) ||
                (ParentNode.Processor.WriteMarkup == MarkupType.dontRemove)))
            {
                ParentNode = ParentNode.Parent;
            }

            // Decide, based on ancestor commands and defaults whether to remove or not
            if (null == ParentNode)
            {
                this.writeMarkup = NodeWriteMarkup;   // Use the default value
            }
            else
            {
                this.writeMarkup = ParentNode.Processor.WriteMarkup;
            }
        }

        private void SetEncodedAs()
        {
            // Need to find out how this node is encoded
            encodingType = Coder.EncodingType.None;
            SchemaAttributeCommands.GetEncoding(oSettings, oEleId, out encodingType);

            if ((null != oEleId) && (encodingType == Coder.EncodingType.None))
            {
                // See if it a built in encoded type
                //XmlSchemaType SType = XMLHelper.GetTypeFromSchema(Name, oSchemaSet, null);
                //if (oEleId.SchemaType is XmlSchemaSimpleType)
                ElementDBEntry oElementDBEntry = oSettings.ObjectDB[oEleId] as ElementDBEntry;
                if (oElementDBEntry.IsSimpleType)
                {
                    XmlSchemaSimpleType SSType = oElementDBEntry.SchemaType as XmlSchemaSimpleType;
                    if (SSType.TypeCode == XmlTypeCode.Base64Binary)
                        encodingType = Coder.EncodingType.Base64;
                    else if (SSType.TypeCode == XmlTypeCode.HexBinary)
                        encodingType = Coder.EncodingType.Hex;
                    TypeCode = XMLHelper.ConvertXmlTypeCodeToDataSchemaType(SSType.TypeCode);
                }
            }
        }

        private void SetOutputAs()
        {
            // Need to note the format of the output for this node, we always get a default value
            SchemaAttributeCommands.GetOutputAs(oSettings, oEleId, out outputType);
        }

        /// <summary>
        /// Sets whether the node should be serialised for output.
        /// </summary>
        private void SetDontOutput()
        {
            SchemaAttributeCommands.GetDontOutput(oSettings, oEleId, out bDontOutput);
        }
        
        private void SetSchemaAttributeInstructions()
        {
            SetMarkup();
            SetEncodedAs();
            SetOutputAs();
            SetDontOutput();
        }

        public void AddXMLProcessingInstructions(List<String> PIs)
        {
            for (int i = 0; i < PIs.Count; i++)
            {
                PItext += (PIs[i] + " ");
            }

            // Match on any non-whitespace character.
            Regex reg = new Regex(@"\S+");

            if((null == PItext) || !reg.IsMatch(PItext))
            {
                // Odd, but no harm done
                return;
            }

            MatchCollection Matches = reg.Matches(PItext);
            
            for(int i = 0; i < Matches.Count; i++)
            {
                String Command = "";
                String Value = "";
                if(!XMLProcInstCommands.CheckCommandExists(Matches[i].Value, out Command, out Value))
                    continue;

                // Process Command
                ProcessInstruction(Command, Value);
            }
        }

        private void ProcessInstruction(String Command, String Value)
        {
            switch (Command)
            {
                case "Order":
                    Order = String.Copy(Value);
                    return;
                case "Id":
                    NodeId.Add(Value);
                    return;
                case "TargetNode":
                    TargetNode = true;
                    return;
                default:
                    break;
            }

            // Assume its a processing instruction
            ProcessorFnAssigner.Assign(Command, Value, ref RefId, ref ProcessingFn);
        }

        public bool ShouldWriteMarkup()
        {
            if ((writeMarkup == MarkupType.dontRemove) || (writeMarkup == MarkupType.dontRemoveIncludingChildNodes))
            {
                return true;
            }
            return false;
        }

        MarkupType WriteMarkup
        {
            get
            {
                return writeMarkup;
            }
        }

        /// <summary>
        /// Update the value of all nodes with XML PI
        /// </summary>
        //public static void Process(OutputNode RootNode)
        //{
        //    // Make a list of nodes with IDs and nodes that need to be processed
        //    List<OutputNode> NodesWithIds = new List<OutputNode>();
        //    List<OutputNode> NodesWithProcFns = new List<OutputNode>();
        //    NodeProcessor.AddNodesToLists(RootNode, NodesWithIds, NodesWithProcFns);
            
        //    // Create sorted list of paired nodes
        //    NodeOrderComparer comparer = new NodeOrderComparer();
        //    SortedList<OutputNode, OutputNode> NodeProcList = new SortedList<OutputNode,OutputNode>(NodesWithProcFns.Count, comparer);
        //    for(int i = 0; i < NodesWithProcFns.Count; i++)
        //    {
        //        bool FoundNodeId = false;
        //        String RefId = NodesWithProcFns[i].Processor.RefId;
        //        for(int j = 0; j < NodesWithIds.Count; j++)
        //        {
        //            // Nodes can have more than one Id, but there is still only the one node
        //            for (int k = 0; k < NodesWithIds[j].Processor.NodeId.Count; k++)
        //            {
        //                if (RefId == NodesWithIds[j].Processor.NodeId[k])
        //                {
        //                    NodeProcList.Add(NodesWithProcFns[i], NodesWithIds[j]);
        //                    FoundNodeId = true;
        //                    break;
        //                }
        //            }
        //        }
        //        if (!FoundNodeId)
        //            Log.Write(MethodBase.GetCurrentMethod(), "Could not find a node with RefId = '" + RefId + "'", Log.LogType.Warning);
        //    }

        //    // Go through list and apply processing functions
        //    for(int i = 0; i < NodeProcList.Count; i++)
        //    {
        //        // A special PI is placed before a node if it is the target of fuzzing (simple types), thus we don't update it.
        //        if (NodeProcList.Keys[i].TargetNode)
        //            continue;
        //        MemoryStream OutputStream = new MemoryStream();
        //        NodeProcList.Values[i].Serialise(OutputStream, true, 0, false);
        //        byte[] rawbytes = OutputStream.ToArray();
        //        NodeProcList.Keys[i].Processor.ProcessingFn(ref rawbytes, NodeProcList.Values[i], NodeProcList.Keys[i]);
                
        //        // Need to validate this new value to ensure it does not violate the XML schema
        //        TypeRestrictor Restrictor = NodeProcList.Keys[i].Restrictor;
        //        //if (!Restrictor.Validate(Coder.DecodeToInput(rawbytes, NodeProcList.Keys[i].TypeCode, NodeProcList.Keys[i].OutputAs, NodeProcList.Keys[i].OutputEncoding)))
        //        String NewValue = NodeProcList.Keys[i].OutputEncoding.GetString(rawbytes);
        //        if (!Restrictor.Validate(NewValue))
        //        {
        //            throw new ValidateFailedException();
        //        }

        //        NodeProcList.Keys[i].ResetWriteElementValue(NewValue);
                
        //        // Determine if the updated Node (Key) was a child of the subject Node (Value), if so we need to reset the Element Value
        //        // of the subject Node (and its descendants), otherwise when we serialise it for real, it will think it has already been 
        //        // serialised, but in fact contains a possibly incorrect value, since the updated Node was a child of it.  This situation 
        //        // occurs when the size field of a block of data is in that block of data and is of a fixed size e.g. 2 bytes.
        //        //if (NodeProcList.Keys[i].HasAncestor(NodeProcList.Values[i]))
        //        //    NodeProcList.Keys[i].ResetElementValueToAncestor(NodeProcList.Values[i]);

        //        // We reset the element value of all parents up to root.  Note this will not reset all siblings in the hierarchy so we
        //        // still benefit from not serialising nodes more than once.  We do this to ensure that any changes made by the processors
        //        // will appear in the output and not be missed due to previously serialised nodes.
        //        NodeProcList.Keys[i].ResetElementValueToRoot();
        //    }

        //    // Dereference these vars
        //    NodesWithIds = null;
        //    NodesWithProcFns = null;
        //    NodeProcList = null;
        //}

        /// <summary>
        /// Recursively create a list of all Nodes with Ids and all Nodes with output Processing Instructions
        /// </summary>
        public static void AddNodesToLists(OutputNode Node, List<OutputNode> NodesWithIds, List<OutputNode> NodesWithProcFns)
        {
            if ((Node is OutputTextNode) || (Node is OutputCommentNode))
                return;

            if (Node.Processor.NodeId.Count > 0)
                NodesWithIds.Add(Node);

            if (null != Node.Processor.ProcessingFn)
                NodesWithProcFns.Add(Node);

            for (int i = 0; i < Node.Children.Count; i++)
            {
                AddNodesToLists(Node.Children[i], NodesWithIds, NodesWithProcFns);
            }
        }
    }

    class NodeOrderComparer : IComparer<OutputNode>
    {
        #region IComparer<OutputNode> Members

        public int  Compare(OutputNode x, OutputNode y)
        {
 	        int xorder;
            int yorder;
            if(!Int32.TryParse(x.Processor.Order, out xorder))
            {
                Log.Write(MethodBase.GetCurrentMethod(), "Could not convert order '" + x.Processor.Order + "' to an Int32", Log.LogType.Error);
            }
            if(!Int32.TryParse(y.Processor.Order, out yorder))
            {
                Log.Write(MethodBase.GetCurrentMethod(), "Could not convert order '" + x.Processor.Order + "' to an Int32", Log.LogType.Error);
            }
            if(xorder > yorder)
                return 1;
            // We never want to return 0, as this means two items in our SortedList are the same and that is not allowed.  Hence since we process
            // the XML from top to bottom, unless an item is specifically supposed to be before another (indicated by its order), we always
            // add items to the end of the SortedList.
            //if(xorder > yorder)
            //    return 1;
            return -1;
        }

        #endregion
    }
}
